/**
 * Copyright (c) 2007-2008 Nearchos Paspallis
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * AspectSense
 *
 * Middleware for annotation-based, context-aware and adaptive services for
 * mobile and ubiquitous computing
 *
 * This project is an open-source effort, initiated by Nearchos Paspallis as
 * part of his PhD work at the University of Cyprus (http://www.ucy.ac.cy).
 *
 * More information about the project is available at "http://aspectsense.org".
 */

package org.aspectsense.gui.client;

import org.aspectsense.framework.Main;
import org.aspectsense.framework.MonitoringManagement;
import org.aspectsense.gui.util.WebBrowserLaunch;
import org.aspectsense.gui.util.WindowPopups;
import org.aspectsense.gui.util.AboutFrame;
import org.aspectsense.gui.client.tree.ComponentsTreeNode;
import org.aspectsense.gui.client.tree.ServicesTreeNode;
import org.aspectsense.gui.client.tree.DefaultTreeNode;

import javax.swing.*;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeSelectionModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeNode;
import java.util.logging.Logger;
import java.util.HashMap;
import java.util.Date;
import java.util.Enumeration;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.NotBoundException;
import java.rmi.registry.Registry;
import java.awt.*;
import java.awt.event.*;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * @author Nearchos Paspallis [nearchos@cs.ucy.ac.cy]
 *         Date: Jan 8, 2008
 *         Time: 11:06:24 AM
 */
public class ManagementConsole extends JFrame
{
    Logger logger = Logger.getAnonymousLogger();

    public static final String FRAME_TITLE = "AspectSense - Management Console";

    public static final String CONNECT_MESSAGE = "Please select the host IP to connect to!";

    private final JTabbedPane mainTabbedPane = new JTabbedPane();
    private final JPanel statusbarPanel = new JPanel();

    private HashMap<String, Remote> connectedCRs = new HashMap<String, Remote>();

    public ManagementConsole()
    {
        super(FRAME_TITLE);

        setLayout(new BorderLayout());
        add(getJMenuBar(), BorderLayout.NORTH);
        add(mainTabbedPane, BorderLayout.CENTER);
        statusbarPanel.setLayout(new BorderLayout());
        JLabel label = new JLabel();
        label.setForeground(new Color(0,0,127));
        label.setFont(label.getFont().deriveFont(11f));
        label.setText("AspectSense " + Main.COPYRIGHT + ". Version " + Main.VERSION);
        statusbarPanel.add(label, BorderLayout.CENTER);
        statusbarPanel.add(new ClockPanel(), BorderLayout.EAST);
        add(statusbarPanel, BorderLayout.SOUTH);

        addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                exit();
            }
        });
        setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);

        setBounds(200, 100, 640, 480);
        setVisible(true);
    }

    /**
     * The management console's menu bar.
     *
     * @return the management console's menu bar
     */
    public JMenuBar getJMenuBar()
    {
        JMenuBar menuBar = new JMenuBar();

        JMenu fileMenu = new JMenu("File");
        fileMenu.setMnemonic('F');

        JMenuItem exitMenuItem = new JMenuItem(new AbstractAction("Exit") {
            public void actionPerformed(ActionEvent e) {
                exit();
            }
        });
        exitMenuItem.setMnemonic('X');

        JMenu systemMenu = new JMenu("System");
        systemMenu.setMnemonic('S');

        JMenuItem connectMenuItem = new JMenuItem(new AbstractAction("Connect ...") {
            public void actionPerformed(ActionEvent e) {
                connect();
            }
        });
        connectMenuItem.setMnemonic('C');
        JMenuItem disconnectMenuItem = new JMenuItem(new AbstractAction("Disconnect") {
            public void actionPerformed(ActionEvent e) {
                disconnect();
            }
        });
        disconnectMenuItem.setMnemonic('D');

        JMenu helpMenu = new JMenu("Help");
        helpMenu.setMnemonic('H');

        JMenuItem indexMenuItem = new JMenuItem(new AbstractAction("Index (Online)") {
            public void actionPerformed(ActionEvent e) {
                index();
            }
        });
        indexMenuItem.setMnemonic('I');
        JMenuItem aboutMenuItem = new JMenuItem(new AbstractAction("About") {
            public void actionPerformed(ActionEvent e) {
                about();
            }
        });
        aboutMenuItem.setMnemonic('B');

        fileMenu.add(exitMenuItem);

        systemMenu.add(connectMenuItem);
        systemMenu.add(disconnectMenuItem);

        helpMenu.add(indexMenuItem);
        helpMenu.addSeparator();
        helpMenu.add(aboutMenuItem);

        menuBar.add(fileMenu);
        menuBar.add(systemMenu);
        menuBar.add(helpMenu);

        return menuBar;
    }

    // -------------------------- Menu actions ------------------------------ //

    private void connect()
    {
        String hostIpAddress;
        try
        {
            hostIpAddress = InetAddress.getLocalHost().getHostAddress();
        }
        catch(UnknownHostException uhe)
        {
            hostIpAddress = "0.0.0.0";
        }
        JTextField hostTextField = new JTextField(hostIpAddress, 15);
        hostTextField.selectAll();
        JTextField portTextField = new JTextField(Integer.toString(Main.DEFAULT_RMI_REGISTRY_PORT), 15);

        JPanel hostPanel = new JPanel(new BorderLayout());
        hostPanel.add(new JLabel("Host: "), BorderLayout.WEST);
        hostPanel.add(hostTextField, BorderLayout.CENTER);

        JPanel portPanel = new JPanel(new BorderLayout());
        portPanel.add(new JLabel("Port: "), BorderLayout.WEST);
        portPanel.add(portTextField, BorderLayout.CENTER);

        JOptionPane.showMessageDialog(
                this,
                new Object [] {CONNECT_MESSAGE, hostPanel, portPanel},
                "Connect ...",
                JOptionPane.QUESTION_MESSAGE);

        String selectedIpAddress = hostTextField.getText();
        int selectedPort = Integer.parseInt(portTextField.getText());

        // at this point, the selectedIpAddress contains the GUI-selected IP

        if(connectedCRs.containsKey(selectedIpAddress))
        {
            WindowPopups.showWarning(this, "The Visualizer is already "
                    + "connected to the selected IP/Port: " + selectedIpAddress
                    + "/" + selectedPort);
            return;
        }

        try
        {
            Registry remoteRegistry = java.rmi.registry.LocateRegistry.getRegistry(selectedIpAddress, selectedPort);
            Remote remote = remoteRegistry.lookup(Main.ASPECT_SENSE_DISTRIBUTION_BROKER_RMI_ID);

            connectedCRs.put(selectedIpAddress, remote);

            mainTabbedPane.addTab(selectedIpAddress, new InstanceTab(selectedIpAddress));
        }
        catch(RemoteException re)
        {
            WindowPopups.showError(this, new String [] { "Could not connect!", "Make sure that an instance of AspectSense is bound to: ", selectedIpAddress + ":" + selectedPort});
        }
        catch(NotBoundException nbe)
        {
            WindowPopups.showError(this, "There is no Component Repository object bound at the specified IP: " + selectedIpAddress);
        }
        catch(ClassCastException cce)
        {
            WindowPopups.showError(this, "Could not cast the bound object! Make sure the remote location runs the correct version!");
        }
        catch (Exception e)
        {
            WindowPopups.showError(this, "Unexpected error: " + e.getMessage() + " caused by: " + e.getCause());
        }
    }

    private void disconnect()
    {
        int selectedIndex = mainTabbedPane.getSelectedIndex();
        if(selectedIndex == -1) return;
        String selectedIpAddress = mainTabbedPane.getTitleAt(selectedIndex);
        mainTabbedPane.removeTabAt(selectedIndex);
        connectedCRs.remove(selectedIpAddress);
    }

    private void exit()
    {
        Object message = "Are you sure you want to exit?";
        if(mainTabbedPane.getSelectedIndex() != -1) message = new Object [] {"You have at least one connection session active!", message};
        int answer = JOptionPane.showConfirmDialog(this, message, "Confirm exit", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
        if(answer == JOptionPane.YES_OPTION)
        {
            dispose();
            System.exit(0);
        }
    }

    private void index()
    {
        WebBrowserLaunch.openURL(Main.WEBSITE_HELP_URL);
    }

    private void about()
    {
        AboutFrame.show();
    }

    // ------------------------ End of Menu actions ------------------------- //

    private void refreshTab(final String ipAddress,
                            final JTree componentsViewTree,
                            final JTree servicesViewTree,
                            final ApplicationGraph graphView)
            throws RemoteException
    {
        final MonitoringManagement monitoringManagement = (MonitoringManagement) connectedCRs.get(ipAddress);
        final String [] installedComponents = monitoringManagement.getInstalledComponentIDs();

        componentsViewTree.setModel(new DefaultTreeModel(new ComponentsTreeNode(installedComponents, monitoringManagement)));
        servicesViewTree.setModel(new DefaultTreeModel(new ServicesTreeNode(installedComponents, monitoringManagement)));
        graphView.updateModel(monitoringManagement);
    }

    private class InstanceTab extends JPanel implements ActionListener
    {
        private final String ipAddress;

        private JTabbedPane tabbedPane;

        private JTree componentsViewTree = new ComponentsViewTree();
        private JScrollPane componentsViewTreeScrollPane
                = new JScrollPane(componentsViewTree);

        private JTree servicesViewTree = new ServicesViewTree();
        private JScrollPane servicesViewTreeScrollPane
                = new JScrollPane(servicesViewTree);

        private final ApplicationGraph graphView = new ApplicationGraph();
        private final JScrollPane graphViewScrollPane = new JScrollPane(graphView);

        private StatusPanel statusPanel;

        InstanceTab(final String ipAddress)
        {
            super(new BorderLayout());
            setOpaque(false);

            this.ipAddress = ipAddress;

            final ToolTipManager toolTipManager = ToolTipManager.sharedInstance();
            toolTipManager.registerComponent(componentsViewTree);
            toolTipManager.registerComponent(servicesViewTree);
            toolTipManager.registerComponent(graphView);

            graphView.setEditable(false);
            graphView.setMoveable(false);

            try
            {
                componentsViewTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
                servicesViewTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);

                statusPanel = new StatusPanel(this);
                add(statusPanel, BorderLayout.NORTH);

                refreshTab(ipAddress, componentsViewTree, servicesViewTree, graphView);

                tabbedPane = new JTabbedPane(JTabbedPane.BOTTOM);
                tabbedPane.addTab("Components View", componentsViewTreeScrollPane);
                tabbedPane.addTab("Services View", servicesViewTreeScrollPane);
                tabbedPane.addTab("Graph View", graphViewScrollPane);

                add(tabbedPane, BorderLayout.CENTER);
            }
            catch(RemoteException re)
            {
                add(new JScrollPane(new JTextArea("Error! - " + re.getMessage())));
            }
        }

        public void actionPerformed(ActionEvent e)
        {
            if(e.getActionCommand().equals("Refresh"))
            {
                try
                {
                    refreshTab(ipAddress, componentsViewTree, servicesViewTree, graphView);
                    statusPanel.setDate(new Date());
                }
                catch(RemoteException re)
                {
                    WindowPopups.showError(this, new String [] {"Error while refreshing info from " + ipAddress, "Disconnecting!"});
                    disconnect();
                }
            }
            else if(e.getActionCommand().equals("ExpandAll"))
            {
                final int selectedIndex = tabbedPane.getSelectedIndex();

                switch (selectedIndex)
                {
                    case  0:
                    {
                        // selected the componentsViewTree
                        expandAll(componentsViewTree, true);
                        break;
                    }
                    case 1:
                    {
                        // selected the servicesViewTree
                        expandAll(servicesViewTree, true);
                        break;
                    }
                }
            }
            else if(e.getActionCommand().equals("CollapseAll"))
            {
                final int selectedIndex = tabbedPane.getSelectedIndex();

                switch (selectedIndex)
                {
                    case  0:
                    {
                        // selected the componentsViewTree
                        expandAll(componentsViewTree, false);
                        break;
                    }
                    case 1:
                    {
                        // selected the servicesViewTree
                        expandAll(servicesViewTree, false);
                        break;
                    }
                }
            }
            else
            {
                logger.warning("Unknown action: " + e.getActionCommand());
            }
        }
    }

    private class StatusPanel extends JPanel
    {
        private JLabel dateLabel = new JLabel();

        StatusPanel(final InstanceTab instanceTab)
        {
            super(new BorderLayout());

            setBorder(BorderFactory.createEtchedBorder());

            setDate(new Date());
            dateLabel.setForeground(Color.GRAY);
            dateLabel.setFont(dateLabel.getFont().deriveFont(10f));

            add(dateLabel, BorderLayout.CENTER);

            final JButton expandAllButton = new JButton("ExpandAll") {
                public Insets getInsets() { return new Insets(1, 4, 1, 4); }
            };
            final JButton collapseAllButton = new JButton("CollapseAll") {
                public Insets getInsets() { return new Insets(1, 4, 1, 4); }
            };
            final JButton refreshButton = new JButton("Refresh") {
                public Insets getInsets() { return new Insets(1, 4, 1, 4); }
            };
            final JPanel buttonsPanel = new JPanel(new BorderLayout());
            buttonsPanel.add(expandAllButton, BorderLayout.WEST);
            buttonsPanel.add(collapseAllButton, BorderLayout.CENTER);
            buttonsPanel.add(refreshButton, BorderLayout.EAST);
            add(buttonsPanel, BorderLayout.EAST);

            expandAllButton.addActionListener(instanceTab);
            collapseAllButton.addActionListener(instanceTab);
            refreshButton.addActionListener(instanceTab);
        }

        private void setDate(final Date date)
        {
            dateLabel.setText("Last updated " + date.toString());
        }
    }

    private class ClockPanel extends JLabel implements Runnable
    {
        ClockPanel()
        {
            super();

            setFont(getFont().deriveFont(11f));

            new Thread(this).start();
        }

        public void run()
        {
            while(true)
            {
                setText(new Date().toString());

                try { Thread.sleep(500); } catch(InterruptedException ie) { break; }
            }
        }
    }

    private class ComponentsViewTree extends JTree
    {
        public Insets getInsets()
        {
            return new Insets(4, 4, 4, 4);
        }

        public String getToolTipText(final MouseEvent event)
        {
            final TreePath treePath = getPathForLocation(event.getX(), event.getY());
            if(treePath != null)
            {
                final Object lastPathComponent = treePath.getLastPathComponent();
                if(lastPathComponent instanceof DefaultTreeNode)
                {
                    return ((DefaultTreeNode) treePath.getLastPathComponent()).getTooltip();
                }
            }

            return null;
        }
    }

    private class ServicesViewTree extends JTree
    {
        public Insets getInsets()
        {
            return new Insets(4, 4, 4, 4);
        }

        public String getToolTipText(final MouseEvent event)
        {
            final TreePath treePath = getPathForLocation(event.getX(), event.getY());
            if(treePath != null)
            {
                final Object lastPathComponent = treePath.getLastPathComponent();
                if(lastPathComponent instanceof DefaultTreeNode)
                {
                    return ((DefaultTreeNode) treePath.getLastPathComponent()).getTooltip();
                }
            }

            return null;
        }
    }

    /**
     * Expands or collapses all the nodes of the specified tree.
     *
     * @param tree the tree to be expanded or collapsed
     * @param expand a boolean indicating whether the tree is to be expanded
     * (true) or collapsed (false)
     */
    public void expandAll(final JTree tree, final boolean expand)
    {
        final TreeNode root = (TreeNode)tree.getModel().getRoot();

        // Traverse tree from root
        expandAll(tree, new TreePath(root), expand);
    }

    private void expandAll(final JTree tree, final TreePath parent, final boolean expand)
    {
        // Traverse children
        final TreeNode node = (TreeNode)parent.getLastPathComponent();
        if (node.getChildCount() >= 0)
        {
            for(final Enumeration enumeration = node.children(); enumeration.hasMoreElements(); )
            {
                final TreeNode treeNode = (TreeNode)enumeration.nextElement();
                final TreePath path = parent.pathByAddingChild(treeNode);
                expandAll(tree, path, expand);
            }
        }

        // Expansion or collapse must be done bottom-up
        if (expand)
        {
            tree.expandPath(parent);
        }
        else
        {
            tree.collapsePath(parent);
        }
    }
}